Skip to content

AsyncAPI 3.x: an E2E test, NCS driven through Kafka - #1810

Open
LautaroPetaccio wants to merge 7 commits into
feature/asyncapi-test-writerfrom
feature/asyncapi-e2e-kafka
Open

LautaroPetaccio wants to merge 7 commits into
feature/asyncapi-test-writerfrom
feature/asyncapi-e2e-kafka

Conversation

@LautaroPetaccio

@LautaroPetaccio LautaroPetaccio commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

Last of the AsyncAPI 3.x series, on top of #1809. Supersedes #1760, which could not be retargeted in place.

The SUT — NCS over Kafka

The NCS case study re-expressed as a message-driven service: six request topics (ncs.<op>.request), each answered on a reply topic with a result message or, for the inputs the REST version answers with a 400, an error message — exactly what ncs-kafka.yaml (already in core's test resources) describes. The routines are ported from EMB's org.restncs.imp and cited at the top of each file; the rejection rules mirror NcsRest. A Spring Boot application with no web layer: it speaks only Kafka, which is the point.

One deliberate deviation from the REST original: a result that is NaN or infinite is answered as an error. JSON has no such numbers, and a string where the contract promises a number would be a false fault.

And one deliberate defect, marked as such in NcsService where it is written: a triangle with a non-positive edge is answered with {"classification": ...}, a shape the document declares nowhere for that reply, where the original routine classifies it as 0 and answers with the declared result. It is there because the reply classifier's fault needs a service that commits it: the message arrives, it correlates, nothing times out, and a client written against the contract still cannot read it. Nothing else in a run notices. Worth knowing before reading the fixture as a faithful port — it is faithful everywhere else.

The driver — the reference executeAsyncApiAction

NcsKafkaController owns the broker (testcontainers, apache/kafka:3.8.0, KRaft), creates the twelve topics up front, and implements the hook as #1738's javadoc sketches it:

  • publish with the correlation id in the header the document names — or stamped into the payload at the declared pointer, for documents that say so;
  • the reply consumer is assigned and positioned at the end before publishing, so a fast reply cannot slip past, and reused for the rest of the run;
  • wait until a record carrying the id arrives or replyTimeoutMs runs out; a reply carrying someone else's id is skipped, one carrying none is taken and reported as correlationMatched = false;
  • fill AsyncApiReplyDto and judge nothing.

It answers getAsyncApiServerAddress with the address this run's broker actually listens on, so a generated test reaches it rather than the address the document declares.

Its reply consumers are dropped when the state is reset, since a search restarted under the same seed mints the same correlation ids again and a reply left on a topic by one test would otherwise answer a request of the next.

kafka-clients and testcontainers-kafka appear only in this test module, managed in the root pom.xml as the doc requires. Nothing is added to any shipped jar.

The search

AsyncApiTestBase joins the other bases in e2e-tests-utils: initAndRun, and helpers that read AsyncApiCallResult off the solution. NcsKafkaEMTest runs 300 action evaluations and asserts:

  • every operation was answered;
  • checkTriangle, bessj and remainder reached their result reply;
  • expint and gammq reached both their result and their error — expint's x and gammq's a state their constraint in prose rather than as a bound, and fisher's x carries none at all, so the search is free to produce one. remainder's bounds are exactly the range the service checks, so no conforming client can provoke its error; bessj's n is fenced the same way, but its x is not, so an error there is reachable, just rare enough not to be asserted. The document says all of this beside each reply, so it does not read as a gap in the search;
  • the undeclared reply is reported against checkTriangle, and nothing against the operations that answer correctly;
  • no NO_REPLY, no PUBLISH_FAILED.

A second search runs with the oracles on and a reply deadline no round trip can meet, so every message goes unanswered and the no-reply fault is reported. Between the two, both sides of that gate are exercised against a live broker.

Because the driver is embedded, the SUT is instrumented, so the search covers lines and branches of a message-driven service as well: 662 targets in 26 seconds, identical on two runs.

The generated suite

The run writes the suite, compiles it and runs it against the same broker, so #1809 is covered by this E2E rather than by unit tests alone. Both JVM languages are covered: Kotlin in the first run, Java in a second one, since a Java half that never compiles is invisible to a Kotlin-only check. The shared compile step picks the compiler by whether the output folder holds .java or .kt sources.

CI

The module starts a broker in a container, so it wants Docker and is slower than the rest. It has an AsyncApiTests profile and a skipAsyncApi flag every other profile turns off, which is how the database E2Es are kept out of runs that have no business paying for them, with a matching CI job.

@LautaroPetaccio
LautaroPetaccio added this pull request to stack #1812 September 30, 2026 17:25
@LautaroPetaccio
LautaroPetaccio force-pushed the feature/asyncapi-e2e-kafka branch 5 times, most recently from ac3b5fe to d553e11 Compare October 9, 2026 18:12
@LautaroPetaccio
LautaroPetaccio force-pushed the feature/asyncapi-e2e-kafka branch from d553e11 to 2145909 Compare October 9, 2026 19:13
@LautaroPetaccio
LautaroPetaccio marked this pull request as ready for review October 9, 2026 19:47
The first search against a real broker. The SUT is the NCS case study
re-expressed as a Kafka service: six request topics answered on six reply
topics with a result or an error, as ncs-kafka.yaml describes. It speaks
only Kafka; there is no HTTP endpoint. Its routines are ported from EMB's
NCS and cited as such.

The driver owns the broker, a testcontainers apache/kafka, and is the
reference implementation of SutController.executeAsyncApiAction: publish
with the correlation id where the document says it goes, wait for the
reply that carries it back, report and do not judge. kafka-clients and
testcontainers-kafka are used only here, in the test tree, and are managed
in the root pom like everything else.

The test asserts that every operation answers and that expint and gammq
each reach both their declared replies. Those two are the operations whose
rejected inputs the schema does not rule out; bessj's and remainder's
bounds are in the schema, so their errors need invalid data, which the
search does not publish yet. 662 targets in 26 seconds, on two runs.
Neither side of the experimental oracle gate was exercised against a live
broker. A second search now runs with the oracles on and a reply deadline no
round trip can meet, so every message goes unanswered and the fault is
reported, while the existing search asserts that a service which answers
correctly has nothing reported against it.

Nothing in the service is broken on purpose for this. The driver ignores
replies whose correlation id does not match the request it is waiting on, so
the late answers piling up on the reply topics cannot be taken for fresh ones.
The search was covered by a run against a real broker; what the run wrote
was not. Both output languages are now generated, compiled and executed
against the same broker: Kotlin in the existing run, Java in a second one,
because a Java half that never compiles is invisible to a Kotlin-only check.

The service gains one deliberate defect, marked as such where it is written:
a non-positive triangle edge is answered with a payload the document declares
nowhere for that reply. It correlates, it arrives, and a client written
against the contract still cannot read it, so it is the one fault only the
reply classifier finds. The suite is expected to report it.

The two replies whose errors no conforming client can reach say so in the
document, beside the reply itself, so the next reader does not take them for
a gap in the search.

The driver compared the correlation location against the two string constants
that have since become an enum, and the shared compile step assumed Kotlin
sources.
The module starts a broker in a container, so it wants Docker and it is
slower than the rest. It gets a profile of its own and a flag every other
profile turns off, which is how the database E2Es are kept out of the runs
that have no business paying for them, and CI gains the job that turns it on.
bessj's error reply is reachable after all. Its n is fenced exactly, which is
what the comment saw, but its x carries no bound at all, and the routine
returns a value that is not a number for a very small one, which the service
answers with an error. Checked by running the routine: n=3 with x=1e-100 is
one of many. remainder's identical claim does hold -- integer arithmetic, no
finiteness check -- so only bessj's is corrected, here and in the E2E that
repeats it. fisher was described as stating its constraint in prose; it
states none.

The driver kept its reply consumers for the whole class, while every test
restarts the search under the same seed and so mints the same correlation
ids again. A reply left on a topic by one test could then answer a request of
the next. They are dropped when the state is reset, and the next one needed
is created at the end of its topic.

A broker that failed to start was never stopped: isSutRunning answers on the
Spring context, which is assigned last, so nothing downstream would have
called stopSut. And the address it answers with was guarded on a field that
is final and never null, so asking before the container was up threw where
the contract says to return null.

A reply the service could not publish was dropped in silence, which the run
then reports as a reply that never came. Said out loud instead.

The triangle reply channel now declares the error message the other five do.
The service answers a rejected request the same way everywhere, so leaving it
out made an ordinary error a second undeclared payload on that topic,
indistinguishable from the deliberate one.

A second on a loaded CI machine is not a generous reply deadline when the
test asserts that nothing went unanswered, so it is three. The module stops
reusing forks, as every other container-backed E2E does, and drops two
dependencies nothing in it uses. The ported NCS routines are listed on the
re-used code page, which is where the policy says they belong.
hamcrest was dropped as unused, which it is -- by this module's own sources.
The suites EvoMaster generates import org.hamcrest.Matchers, and they are
compiled against this module's classpath, so without it the E2E fails at
"unresolved reference 'Matchers'" the moment it tries to compile what the run
wrote. Said in the pom, so the next reader does not drop it again.
@LautaroPetaccio
LautaroPetaccio force-pushed the feature/asyncapi-e2e-kafka branch from 2145909 to c7a0cb2 Compare October 9, 2026 20:55
The comment on testRunEMInJava states what compiling and running the
emitted Java checks.

No behaviour changes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant